10 异步编程
> Last Format Time:8/11/2026 21:45:39
本笔记由两个 JavaScript 教程目录中的同主题内容合并而成;全部旧知识保留在“旧笔记知识全集(按来源保留)”中,并由导航逐项索引。
当前前端开发关键要点
> [!important] 学习优先级:P0 > - 必须掌握事件循环、调用栈、任务与微任务,能够解释 Promise 回调的执行顺序。 > - 使用 async/await 组织流程,并明确串行、并行、竞态、取消、超时和错误传播。 > - 根据容错需求选择 Promise.all、allSettled、race 或 any,避免无意的串行等待。 > - 组件卸载或请求替换时使用 AbortController 等机制处理取消,防止陈旧结果覆盖新状态。
知识点导航
[现代JS教程与阮一峰ES6 · 6异步编程/Promise与回调](./10 异步编程.md)
- 回调
- Promise
- Promise链
- 错误处理
- Promise的静态方法(API)
- all
- allSettled:
- Promisification
- 微任务
- async/await
[廖雪峰教程 · 7浏览器/7.5 Promise](./10 异步编程.md)
- Promise
- 手撸代码
- 💡 Promise 基础
- 🤔
Promise是如何实现异步的? - 执行顺序解析
- 📌 核心要点
- 🤝 Promise.all 实现
- 🏎️ Promise.race 实现
- ✨ Promise.any 实现
- 📝 总结
- 🛠️ 实现 Promise.map
- 📝 使用示例
- ✨ 进阶:增加并发限制
- 📌 总结
[廖雪峰教程 · 7浏览器/7.6 async](./10 异步编程.md)
- async
现代前端补充与纠错
> [!info] 修改标记 > - 【修正】:旧教程中错误、过时或容易误导的内容。 > - 【补充】:旧教程未覆盖、但当前前端开发需要掌握的内容。 > - 【修正代码】:替换或校正了旧代码示例。
> [!warning] 下方 Promise 手写题保留原教程上下文;空输入、任意 iterable、错误顺序和并发边界已直接修正并标成 【修正代码】。
事件循环
- 【补充】 Promise 回调与
queueMicrotask进入微任务队列;定时器、消息和用户交互通常形成任务。一次任务结束后会清空微任务,再由浏览器选择渲染时机。 - 【补充】 微任务递归排队可能阻塞渲染和后续任务;长计算需要拆分、调度或交给 Worker。
- 【补充】 完整浏览器模型见 事件循环eventloop。
Promise 组合器边界
- 【修正】
Promise.all(iterable)接受任意可迭代对象;空输入会立即以[]fulfilled,结果保持输入顺序。 - 【修正】
Promise.any(iterable)空输入会以AggregateErrorrejected;errors按输入顺序排列,不按完成顺序。 - 【补充】
allSettled适合需要完整结果清单的场景,race只观察第一个落定结果,并不会自动取消其余任务。 - 【补充】 并发池必须处理空任务、非法并发数、同步抛错、结果顺序与取消信号;“限制并发”不等于“超时”或“取消”。
请求与取消
- 【修正】
fetch遇到 HTTP 404/500 通常仍会 fulfilled,应检查response.ok;网络失败、CORS 拒绝或主动中止才会 reject。 - 【补充】 用
AbortController统一取消请求和支持signal的监听器;超时还应处理竞态和过期响应。见 [AbortController ⌚️](../../../浏览器&网络/浏览器环境/Web API/AbortController ⌚️.md)。 - 【补充】 全局关注未处理拒绝可监听
unhandledrejection,但它是兜底监控,不替代调用链上的错误处理。
旧笔记知识全集(按来源保留)
现代JS教程与阮一峰ES6 · 6异步编程/Promise与回调
回调
使用回调的方式来进行异步编程:
function loadScript(src, callback) {
console.log('finished'+src);
setTimeout(callback, 500);
}
loadScript('1',() => {console.log('callback');});
在回调中回调:
function loadScript(src, callback) {
console.log('finished'+src);
setTimeout(callback, 500);
}
//回调地狱
loadScript(
'1',
() => {
console.log('callback');
loadScript(
'2',
() => {
console.log('callback');
loadScript(
'3',
() => {
console.log('callback');
loadScript(
'4',
() => {
console.log('callback');
}
);
}
);
}
);
}
);
/**
* finished1
callback
finished2
callback
finished3
callback
finished4
callback
*/
Promise
是(单线程并发)解决异步编程回调地狱的一种方案。是一个特殊的对象,Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。
Promise构造函数接受一个函数executor作为参数,该函数的两个参数分别是resolve和reject。它们是两个回调函数,由 JavaScript 引擎提供,不用自己部署。
function timeout(ms) {
return new Promise((resolve, reject) => {
setTimeout(resolve, ms, 'done');
});
}
timeout(100).then((value) => {
console.log(value);
});
当 executor 获得了结果,无论是早还是晚都没关系,在函数体的内部它应该调用以下回调之一:
resolve(value)—— 如果任务成功完成并带有结果value。reject(error)—— 如果出现了 error,error即为 error 对象。
由 new Promise 构造器返回的 promise 对象具有以下内部属性:
state—— 最初是"pending",然后在resolve被调用时变为"fulfilled",或者在reject被调用时变为"rejected"。result—— 最初是undefined,然后在resolve(value)被调用时变为value,或者在reject(error)被调用时变为error。
其中,pending->fulfilled和pending->rejected是仅有的两种状态转换,代表着异步操作的结果。executor 只能调用一个 resolve 或一个 reject。任何状态的更改都是最终的。所有其他的再对 resolve 和 reject 的调用都会被忽略
 resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操作成功时调用,并==将异步操作的结果,作为参数传递出去==
reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并==将异步操作报出的错误,作为参数传递出去==
//用于随机生成一个图片
function Image() {
let result = Math.random();
if (result > 0.5) {
this.value = true;
} else {
this.value = false;
};
}
let promise = new Promise(function (resolve, reject) {
let image = new Image();
console.log(image.value);
if (image.value) {
resolve('loaded');
} else {
reject(new Error("shit!!!"));
}
});
注意,调用resolve或reject并不会终结 Promise 的参数函数的执行。一般来说,调用resolve或reject以后,Promise 的使命就完成了,后继操作应该放到then方法里面,而不应该直接写在resolve或reject的后面。所以,最好在它们前面加上return语句,这样就不会有意外。
new Promise((resolve, reject) => {
resolve(1);
console.log(2);
}).then(r => {
console.log(r);
});
// 2
// 1
Promise 对象的 state 和 result 属性都是内部的。我们无法直接访问它们。但我们可以对它们使用 .then/.catch/.finally 方法。我们在下面对这些方法进行了描述。
Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数。then方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为resolved时调用,第二个回调函数是Promise对象的状态变为rejected时调用。这两个函数都是可选的。它们都接受Promise对象传出的值作为参数。
promise.then(
function(result) { /* handle a successful result */ },
function(error) { /* handle an error */ }
);
promise.then(
result => { console.log(result) },//loaded
error => { console.log(error) }//Error: shit!!!
)
.catch(f) 调用是 .then(null, f) 的完全的模拟,它只是一个==简写形式==。 一般使用then来处理结果,使用catch来处理错误。
finally 的功能是设置一个处理程序在前面的操作完成后,执行清理/终结。
new Promise((resolve, reject) => {
return resolve(1);
// 后面的语句不会执行
console.log(2);
})
getJSON('/posts.json').then(function(posts) {
// ...
}).catch(function(error) {
// 处理 getJSON 和 前一个回调函数运行时发生的错误
console.log('发生错误!', error);
});
上面代码中,getJSON()方法返回一个 Promise 对象,如果该对象状态变为resolved,则会调用then()方法指定的回调函数;如果异步操作抛出错误,状态就会变为rejected,就会调用catch()方法指定的回调函数,处理这个错误。另外,then()方法指定的回调函数,如果运行中抛出错误,也会被catch()方法捕获。
p.then((val) => console.log('fulfilled:', val))
.catch((err) => console.log('rejected', err));
// 等同于
p.then((val) => console.log('fulfilled:', val))
.then(null, (err) => console.log("rejected:", err));
// 写法二
const promise = new Promise(function(resolve, reject) {
reject(new Error('test'));
});
promise.catch(function(error) {
console.log(error);
});
如果 Promise 状态已经变成resolved,再抛出错误是无效的。
const promise = new Promise(function(resolve, reject) {
resolve('ok');
throw new Error('test');
});
promise
.then(function(value) { console.log(value) })
.catch(function(error) { console.log(error) });
// ok
上面代码中,Promise 在resolve语句后面,再抛出错误,不会被捕获,等于没有抛出。因为 Promise 的状态一旦改变,就永久保持该状态,不会再变了。一般来说,不要在then()方法里面定义 Reject 状态的回调函数(即then的第二个参数),总是使用catch方法。Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个catch语句捕获。
getJSON('/post/1.json').then(function(post) {
return getJSON(post.commentURL);
}).then(function(comments) {
// some code
}).catch(function(error) {
// 处理前面三个Promise产生的错误
});
跟传统的try/catch代码块不同的是,如果没有使用catch()方法指定错误处理的回调函数,Promise 对象抛出的==错误不会传递到外层代码,即不会有任何反应。==
const someAsyncThing = function () {
return new Promise(function (resolve, reject) {
// 下面一行会报错,因为x没有声明
resolve(x + 2);
});
};
someAsyncThing()
.then(function () {
console.log('everything is great');
})
.catch((err) => {
console.error(err);
})
经过我的实验,是这样的,这样有bug了也不会报错 一般总是建议,Promise 对象后面要跟catch()方法,这样可以处理 Promise 内部发生的错误。catch()方法返回的还是一个 Promise 对象,因此后面还可以接着调用then()方法。
const someAsyncThing = function() {
return new Promise(function(resolve, reject) {
// 下面一行会报错,因为x没有声明
resolve(x + 2);
});
};
someAsyncThing()
.catch(function(error) {
console.log('oh no', error);
})
.then(function() {
console.log('carry on');
});
// oh no [ReferenceError: x is not defined]
// carry on
finally()方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。
promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});
Promise链
使用链式调用来实现对结果和错误的精细处理:
let promise = new Promise(function (res,rej) {
res(1);
})
promise.then((res) => {
console.log(res);//1
return res * 2;
}).then((res) => {
console.log(res);//2
return res * 2;
}).finally(() => {
console.log('finished!!!');//finished!!!
})

如果将一个promise对象上添加多个then,他们是彼此独立的,并不是一个链式的结构。 
.then(handler) 中所使用的处理程序(handler)可以创建并返回一个 promise。
确切地说,处理程序返回的不完全是一个 promise,而是返回的被称为 “thenable” 对象 —— 一个具有方法 .then 的任意对象。它会被当做一个 promise 来对待。
这个想法是,第三方库可以实现自己的“promise 兼容(promise-compatible)”对象。它们可以具有扩展的方法集,但也与原生的 promise 兼容,因为它们实现了 .then 方法。
let promise = fetch(url);
执行这条语句,向 url 发出网络请求并返回一个 promise。当远程服务器返回 header(是在 全部响应加载完成前)时,该 promise 使用一个 response 对象来进行 resolve。
fetch('/article/promise-chaining/user.json')
// 当远程服务器响应时,下面的 .then 开始执行
.then(function(response) {
// 当 user.json 加载完成时,response.text() 会返回一个新的 promise
// 该 promise 以加载的 user.json 为 result 进行 resolve
return response.text();
})
.then(function(text) {
// ……这是远程文件的内容
alert(text); // {"name": "iliakan", "isAdmin": true}
});
从 fetch 返回的 response 对象还包含 response.json() 方法,该方法可以读取远程数据并将其解析为 JSON。
// 同上,但使用 response.json() 将远程内容解析为 JSON
fetch('/article/promise-chaining/user.json')
.then(response => response.json())
.then(user => alert(user.name)); // iliakan,获取到了用户名
promise.then(f1, f2);
promise.then(f1).catch(f2);
这两个例子是不一样的: 不同之处在于,如果 f1 中出现 error,那么在这儿它会被 .catch 处理 .then 将 result/error 传递给下一个 .then/.catch。所以在第一个例子中,在下面有一个 catch,而在第二个例子中并没有 catch,所以 error 未被处理。
错误处理
正如我们已经注意到的,链尾端的 .catch 的表现有点像 try..catch。我们可能有许多个 .then 处理程序,然后在尾端使用一个 .catch 处理上面的所有 error。
在常规的 try..catch 中,我们可以分析 error,如果我们无法处理它,可以将其再次抛出。对于 promise 来说,这也是可以的。
如果我们在 .catch 中 throw,那么控制权就会被移交到下一个最近的 error 处理程序。如果我们处理该 error 并正常完成,那么它将继续到最近的成功的 .then 处理程序。
new Promise(function(resolve, reject) {
setTimeout(() => {
throw new Error("Whoops!");
}, 1000);
}).catch(alert);
不会进行处理,这里的错误并不是在 executor 运行时生成的,而是在稍后生成的。因此,promise 无法处理它。
Promise的静态方法(API)
all
如果任意一个 promise 被 reject,由 Promise.all 返回的 promise 就会立即 reject,并且带有的就是这个 error。 假设我们希望并行执行多个 promise,并等待所有 promise 都准备就绪。
let promise = Promise.all(iterable);
Promise.all 接受一个可迭代对象(通常是一个==数组项为 promise 的数组==),并返回一个新的 promise。
当所有给定的 promise 都 resolve 时,新的 promise 才会 resolve,并且其结果数组将成为新 promise 的结果。
let names = ['iliakan', 'remy', 'jeresig'];
let requests = names.map(name => fetch(`https://api.github.com/users/${name}`));
Promise.all(requests)
.then(responses => {
// 所有响应都被成功 resolved
for(let response of responses) {
alert(`${response.url}: ${response.status}`); // 对应每个 url 都显示 200
}
return responses;
})
// 将响应数组映射(map)到 response.json() 数组中以读取它们的内容
.then(responses => Promise.all(responses.map(r => r.json())))
// 所有 JSON 结果都被解析:"users" 是它们的数组
.then(users => users.forEach(user => alert(user.name)));
如果其中一个 promise 被 reject,Promise.all 就会立即被 reject,==完全忽略==列表中==其他的 promise==。它们的结果也被忽略。
例如,像上面那个例子,如果有多个同时进行的 fetch 调用,其中一个失败,其他的 fetch 操作仍然会继续执行,但是 Promise.all 将不会再关心(watch)它们。它们可能会 settle,但是它们的结果将被忽略。
Promise.all 没有采取任何措施来取消它们,因为 promise 中没有“取消”的概念。在 另一个章节 中,我们将介绍可以帮助我们解决这个问题(译注:指的是“取消” promise)的 AbortController,但它不是 Promise API 的一部分。
allSettled:
Promise.allSettled 等待所有的 promise 都被 settle,无论结果如何。结果数组会是这样的:
- 对成功的响应,结果数组对应元素的内容为
{status:"fulfilled", value:result}, - 对出现 error 的响应,结果数组对应元素的内容为
{status:"rejected", reason:error}。
let urls = [
'https://api.github.com/users/iliakan',
'https://api.github.com/users/remy',
'https://no-such-url'
];
Promise.allSettled(urls.map(url => fetch(url)))
.then(results => { // (*)
results.forEach((result, num) => {
if (result.status == "fulfilled") {
alert(`${urls[num]}: ${result.value.status}`);
}
if (result.status == "rejected") {
alert(`${urls[num]}: ${result.reason}`);
}
});
});
[
{status: 'fulfilled', value: ...response...},
{status: 'fulfilled', value: ...response...},
{status: 'rejected', reason: ...error object...}
]
没看完
Promisification
指将一个接受回调的函数转换为一个返回 promise 的函数。
//改造前
function loadScript(src, callback) {
let script = document.createElement('script');
script.src = src;
script.onload = () => callback(null, script);
script.onerror = () => callback(new Error(`Script load error for ${src}`));
document.head.append(script);
}
// 用法:
// loadScript('path/script.js', (err, script) => {...})
//改造后
let loadScriptPromise = function(src) {
return new Promise((resolve, reject) => {
loadScript(src, (err, script) => {
if (err) reject(err);
else resolve(script);
});
});
};
// 用法:
// loadScriptPromise('path/script.js').then(...)
在实际开发中,我们可能需要 promise 化很多函数,所以使用一个 helper(辅助函数)很有意义。 我们将其称为 promisify(f):它接受一个需要被 promise 化的函数 f,并返回一个包装(wrapper)函数。
function promisify(f) {
return function (...args) { // 返回一个包装函数(wrapper-function) (*)
return new Promise((resolve, reject) => {
function callback(err, result) { // 我们对 f 的自定义的回调 (**)
if (err) {
reject(err);
} else {
resolve(result);
}
}
args.push(callback); // 将我们的自定义的回调附加到 f 参数(arguments)的末尾
f.call(this, ...args); // 调用原始的函数
});
};
}
// 用法:
let loadScriptPromise = promisify(loadScript);
loadScriptPromise(...).then(...);
微任务
promise 的处理程序 .then、.catch 和 .finally 都是异步的。 即便一个 promise 立即被 resolve,.then、.catch 和 .finally 在promise后的代码也会在这些处理程序之前被执行:
let promise = Promise.resolve();
promise.then(() => alert("promise done!"));
alert("code finished"); // 这个 alert 先显示
异步任务需要适当的管理。为此,ECMA 标准规定了一个内部队列 PromiseJobs,通常被称为“微任务队列(microtask queue)”(V8 术语):
- 队列(queue)是先进先出的:首先进入队列的任务会首先运行。
- 只有在 JavaScript 引擎中没有其它任务在运行时,才开始执行任务队列中的任务。
当一个 promise 准备就绪时,它的 .then/catch/finally 处理程序就会被放入队列中:但是它们不会立即被执行。当 JavaScript 引擎执行完当前的代码,它会从队列中获取任务并执行它。 
如果一个 promise 的 error 未被在微任务队列的末尾进行处理,则会出现“未处理的 rejection”。
async/await
promise在解决地狱回调的同时,但在使用时有一些不优雅 async/await 是以更舒适的方式使用 promise 的一种特殊语法,同时它也非常易于理解和使用。
使用async关键字在函数前,函数就会总是返回一个promise对象。其他值将自动被包装在一个 resolved 的 promise 中。
await只在 async 函数内工作,关键字 await 让 JavaScript 引擎等待直到 promise 完成(settle)并返回结果
像 promise.then 那样,await 允许我们使用 thenable 对象(那些具有可调用的 then 方法的对象)。这里的想法是,第三方对象可能不是一个 promise,但却是 promise 兼容的:如果这些对象支持 .then,那么就可以对它们使用 await。
要声明一个 class 中的 async 方法,只需在对应方法前面加上 async 即可:
class Waiter {
async wait() {
return await Promise.resolve(1);
}
}
new Waiter()
.wait()
.then(alert); // 1(alert 等同于 result => alert(result))
async function f() {
try {
let response = await fetch('http://no-such-url');
} catch(err) {
alert(err); // TypeError: failed to fetch
}
}
f();
async function f() {
let response = await fetch('http://no-such-url');
}
// f() 变成了一个 rejected 的 promise
f().catch(alert); // TypeError: failed to fetch // (*)
当我们使用 async/await 时,几乎就不会用到 .then 了,因为 await 为我们处理了等待。并且我们使用常规的 try..catch 而不是 .catch。这通常更加方便。
使用同步的写法来实现异步的效果,十分的优雅 
 这个函数中使用 await 是因为 axios(...) 会返回一个 Promise 对象,而 await 的作用正是等待这个 Promise 完成(成功或失败),并获取其返回结果。
具体原因如下:
axios是基于 Promise 的请求库,调用axios(...)后会立即返回一个 Promise(此时请求正在异步执行),而非直接返回接口响应数据。若不使用
await,代码会继续执行后续的list.value = res.data.list,但此时res还未获取到真正的响应结果(因为请求还没完成),会导致res是一个未完成的 Promise,进而报错。使用
await后,函数会暂停执行,等待axios的请求完成并拿到响应结果后,再将结果赋值给res,确保后续代码能正确使用接口返回的数据。
简单说,await 是为了将异步请求的结果 “同步化” 处理,让代码按照 “发送请求→等待结果→处理结果” 的逻辑顺序执行,避免异步操作导致的数据获取时机问题。
廖雪峰教程 · 7浏览器/7.5 Promise
Promise
在JavaScript的世界中,所有代码都是单线程执行的。
由于这个“缺陷”,导致JavaScript的所有网络操作,浏览器事件,都必须是异步执行。异步执行可以用回调函数实现:
function callback() {
console.log('Done');
}
console.log('before setTimeout()');
setTimeout(callback, 1000); // 1秒钟后调用callback函数
console.log('after setTimeout()');
观察上述代码执行,在Chrome的控制台输出可以看到:
before setTimeout()
after setTimeout()
(等待1秒后)
Done
可见,异步操作会在将来的某个时间点触发一个函数调用。
AJAX就是典型的异步操作:
request.onreadystatechange = function () {
if (request.readyState === 4) {
if (request.status === 200) {
return success(request.responseText);
} else {
return fail(request.status);
}
}
}
把回调函数success(request.responseText)和fail(request.status)写到一个AJAX操作里很正常,但是不好看,而且不利于代码复用。
有没有更好的写法?比如写成这样:
let ajax = ajaxGet('http://...');
ajax.ifSuccess(success)
.ifFail(fail);
这种链式写法的好处在于,先统一执行AJAX逻辑,不关心如何处理结果,然后,根据结果是成功还是失败,在将来的某个时候调用success函数或fail函数。
古人云:“君子一诺千金”,这种“承诺将来会执行”的对象在JavaScript中称为Promise对象。
Promise有各种开源实现,在ES6中被统一规范,由浏览器直接支持。先测试一下你的浏览器是否支持Promise:
Run
我们先看一个最简单的Promise例子:生成一个0-2之间的随机数,如果小于1,则等待一段时间后返回成功,否则返回失败:
function test(resolve, reject) {
let timeOut = Math.random() * 2;
log('set timeout to: ' + timeOut + ' seconds.');
setTimeout(function () {
if (timeOut < 1) {
log('call resolve()...');
resolve('200 OK');
}
else {
log('call reject()...');
reject('timeout in ' + timeOut + ' seconds.');
}
}, timeOut * 1000);
}
这个test()函数有两个参数,这两个参数都是函数,如果执行成功,我们将调用resolve('200 OK'),如果执行失败,我们将调用reject('timeout in ' + timeOut + ' seconds.')。可以看出,test()函数只关心自身的逻辑,并不关心具体的resolve和reject将如何处理结果。
有了执行函数,我们就可以用一个Promise对象来执行它,并在将来某个时刻获得成功或失败的结果:
let p1 = new Promise(test);
let p2 = p1.then(function (result) {
console.log('成功:' + result);
});
let p3 = p2.catch(function (reason) {
console.log('失败:' + reason);
});
变量p1是一个Promise对象,它负责执行test函数。由于test函数在内部是异步执行的,当test函数执行成功时,我们告诉Promise对象:
// 如果成功,执行这个函数:
p1.then(function (result) {
console.log('成功:' + result);
});
当test函数执行失败时,我们告诉Promise对象:
p2.catch(function (reason) {
console.log('失败:' + reason);
});
Promise对象可以串联起来,所以上述代码可以简化为:
new Promise(test).then(function (result) {
console.log('成功:' + result);
}).catch(function (reason) {
console.log('失败:' + reason);
});
可见Promise最大的好处是在异步执行的流程中,把执行代码和处理结果的代码清晰地分离了:
┌──────────────────┐
then │on_resolve(data) {│
┌──────────▶│ // TODO │
│ │} │
┌────────────────────────┐ └──────────────────┘
│ new Promise(async) │
└────────────────────────┘ ┌──────────────────┐
│ │ │on_reject(data) { │
│ └──────────▶│ // TODO │
promise│ catch │} │
│ └──────────────────┘
▼
┌────────────────────────┐
│async(resolve, reject) {│
│ // TODO │
│} │
└────────────────────────┘
Promise还可以做更多的事情,比如,有若干个异步任务,需要先做任务1,如果成功后再做任务2,任何任务失败则不再继续并执行错误处理函数。
要串行执行这样的异步任务,不用Promise需要写一层一层的嵌套代码。有了Promise,我们只需要简单地写:
job1.then(job2).then(job3).catch(handleError);
其中,job1、job2和job3都是Promise对象。
除了串行执行若干异步任务外,Promise还可以并行执行异步任务。
试想一个页面聊天系统,我们需要从两个不同的URL分别获得用户的个人信息和好友列表,这两个任务是可以并行执行的,用Promise.all()实现如下:
let p1 = new Promise(function (resolve, reject) {
setTimeout(resolve, 500, 'P1');
});
let p2 = new Promise(function (resolve, reject) {
setTimeout(resolve, 600, 'P2');
});
// 同时执行p1和p2,并在它们都完成后执行then:
Promise.all([p1, p2]).then(function (results) {
console.log(results); // 获得一个Array: ['P1', 'P2']
});
有些时候,多个异步任务是为了容错。比如,同时向两个URL读取用户的个人信息,只需要获得先返回的结果即可。这种情况下,用Promise.race()实现:
let p1 = new Promise(function (resolve, reject) {
setTimeout(resolve, 500, 'P1');
});
let p2 = new Promise(function (resolve, reject) {
setTimeout(resolve, 600, 'P2');
});
Promise.race([p1, p2]).then(function (result) {
console.log(result); // 'P1'
});
由于p1执行较快,Promise的then()将获得结果'P1'。p2仍在继续执行,但执行结果将被丢弃。
手撸代码
如果我们组合使用Promise,就可以把很多异步任务以并行和串行的方式组合起来执行。
要理解这些 Promise 方法的实现,我们可以从最基础的 Promise 开始,然后逐步深入到 Promise.all、Promise.race 和 Promise.any。
首先需要说明的是,JavaScript 原生并没有 Promise.map 方法。通常所说的 Promise.map 是指对数组中的每个元素执行一个返回 Promise 的函数,并处理这些 Promise。Promise.all 就是实现这一功能的绝佳工具。
下面将通过代码示例,带你一步步了解它们的核心实现原理。
💡 Promise 基础
Promise 是一个代表异步操作最终完成(或失败)及其结果值的对象。一个简单的实现如下:
class SimplePromise {
constructor(executor) {
this.state = 'pending'; // 状态:pending, fulfilled, rejected
this.value = undefined; // 成功时的值
this.reason = undefined; // 失败时的原因
this.onFulfilledCallbacks = []; // 成功回调队列
this.onRejectedCallbacks = []; // 失败回调队列
const resolve = (value) => {
if (this.state === 'pending') {
this.state = 'fulfilled';
this.value = value;
this.onFulfilledCallbacks.forEach(fn => fn());
}
};
const reject = (reason) => {
if (this.state === 'pending') {
this.state = 'rejected';
this.reason = reason;
this.onRejectedCallbacks.forEach(fn => fn());
}
};
try {
executor(resolve, reject);
} catch (error) {
reject(error);
}
}
then(onFulfilled, onRejected) {
// 简化版 then 方法,处理状态和回调
if (this.state === 'fulfilled') {
onFulfilled(this.value);
}
if (this.state === 'rejected') {
onRejected(this.reason);
}
if (this.state === 'pending') {
this.onFulfilledCallbacks.push(() => onFulfilled(this.value));
this.onRejectedCallbacks.push(() => onRejected(this.reason));
}
}
}
在这段代码中,executor(执行器)是你传递给 Promise 构造函数的那个函数。
当你使用 new Promise((resolve, reject) => { ... }) 时,箭头函数 (resolve, reject) => { ... } 就是这个 executor。
你说得对,我们之前实现的 Promise 中的 executor 函数确实是同步执行的。但这并不矛盾,因为 Promise 的异步能力,并非来自于 executor 本身,而是来自于 executor 内部通常会调用的那些真正的异步 API。
简单来说,Promise 本身只是一个“包装器”或“管理器”,它负责管理和通知异步操作的结果,但它自己并不创造异步。
支持链式调用的promise
class ChainablePromise {
constructor(executor) {
this.state = 'pending'
this.value = undefined
this.reason = undefined
this.onFulfilledCallbacks = []
this.onRejectedCallbacks = []
const resolve = (value) => {
if (this.state === 'pending') {
this.state = 'fulfilled'
this.value = value
this.onFulfilledCallbacks.forEach((fn) => fn())
}
}
const reject = (reason) => {
if (this.state === 'pending') {
this.state = 'rejected'
this.reason = reason
this.onRejectedCallbacks.forEach((fn) => fn())
}
}
try {
executor(resolve, reject)
} catch (error) {
reject(error)
}
}
// 核心改造:then 方法
then(onFulfilled, onRejected) {
// 处理未传入回调的情况 - 需要将函数设为默认或透传
onFulfilled =
typeof onFulfilled === 'function' ? onFulfilled : (value) => value
onRejected =
typeof onRejected === 'function'
? onRejected
: (reason) => {
throw reason
}
// 1. 关键!每次调用 .then() 都必须返回一个新的 Promise
return new ChainablePromise((resolve, reject) => {
// 2. 定义一个函数来处理 onFulfilled 回调的执行和结果
const handleFulfillment = () => {
try {
// 执行用户传入的成功回调
const result = onFulfilled(this.value)
// 检查回调的返回值
if (result instanceof ChainablePromise) {
// 如果返回的是一个 Promise,就等待它完成
result.then(resolve, reject)
} else {
// 如果返回的是普通值,就用这个值 resolve 新的 Promise
resolve(result)
}
} catch (error) {
// 如果回调函数内部抛出异常,则 reject 新的 Promise
reject(error)
}
}
// 3. 定义一个函数来处理 onRejected 回调
const handleRejection = () => {
try {
const result = onRejected(this.reason)
if (result instanceof ChainablePromise) {
result.then(resolve, reject)
} else {
resolve(result)
}
} catch (error) {
reject(error)
}
}
// 4. 根据当前 Promise 的状态,决定何时执行回调
if (this.state === 'fulfilled') {
handleFulfillment()
}
if (this.state === 'rejected') {
handleRejection()
}
if (this.state === 'pending') {
// 如果还是 pending 状态,就将回调函数存入队列,等待未来执行
this.onFulfilledCallbacks.push(handleFulfillment)
this.onRejectedCallbacks.push(handleRejection)
}
})
}
catch(onRejected) {
return this.then(null, onRejected)
}
finally(onFinally) {
return this.then(
(value) => {
onFinally()
return value
},
(reason) => {
onFinally()
throw reason
},
)
}
}
const myPromise = new ChainablePromise((resolve, reject) => {
setTimeout(() => {
resolve('success')
}, 2000)
setTimeout(() => {
reject('error')
}, 3000)
})
.then((res) => {
console.log('success:', res)
})
.catch((err) => {
console.log('error:', err)
}).finally(() => {
console.log('finally')
})
一但递归起来就有点搞不明白了,呵呵呵
🤔 Promise 是如何实现异步的?
Promise 的异步能力是“借力”而来的。它依赖于 JavaScript 运行时环境(比如浏览器或 Node.js)提供的原生异步 API。
最常见的就是 setTimeout、XMLHttpRequest (或 fetch)、fs.readFile 等。
让我们来看一个例子:
const myPromise = new Promise((resolve, reject) => {
// 👆 executor 函数是同步执行的
console.log('1. executor 开始执行 (同步)');
// 2. 调用浏览器提供的异步 API
setTimeout(() => {
console.log('3. setTimeout 的回调执行 (异步)');
resolve('任务完成');
}, 1000);
console.log('2. executor 执行完毕 (同步)');
});
console.log('4. Promise 创建完毕,但异步任务还在后台进行');
myPromise.then(result => {
console.log('5. Promise 状态变为 fulfilled,then 回调执行', result);
});
我理解了,promise其实就是将我们还没有执行的逻辑放在了待执行的队列中,在pendding的状态结束的时候就回执行这些转态,太妙了。
执行顺序解析
executor同步执行:new Promise()被调用,executor函数立即开始执行。- 调用
setTimeout:executor内部调用了setTimeout。setTimeout是浏览器提供的一个异步 Web API。当setTimeout被调用时,它会告诉浏览器的计时器线程:“1秒后,把这个回调函数放进任务队列里”。然后,setTimeout函数本身会立即返回,不会阻塞后续代码。 executor执行完毕:executor中的同步代码执行完毕,Promise对象创建完成,状态仍为pending。- 主线程继续:主线程继续执行,打印第4行日志。
- 异步回调入队:1秒后,浏览器的计时器线程将
setTimeout的回调函数(也就是那个包含resolve('任务完成')的函数)放入任务队列 (Task Queue)。 - 事件循环 (Event Loop) 调度:JavaScript 的事件循环会不断检查调用栈是否为空。当主线程的同步任务都完成后,事件循环会从任务队列中取出回调函数,并将其放入调用栈执行。
resolve被调用:setTimeout的回调函数执行,调用了resolve。这时,Promise的状态才从pending变为fulfilled。then回调执行:Promise状态改变后,.then()方法注册的回调函数被放入微任务队列,等待执行,最终打印第5行日志。
📌 核心要点
executor是同步的:它的作用是立即启动一个异步任务。- 异步的来源:真正的异步行为是由
executor内部调用的原生异步 API(如setTimeout、fetch等)提供的。 Promise的角色:Promise是一个“状态机”和“回调管理器”。它通过resolve和reject函数,将异步操作的结果“通知”给后续的.then()或.catch()回调。
所以,Promise 之所以能处理异步,是因为它巧妙地包装了那些底层的、真正的异步机制,并提供了一套更优雅、更易用的链式调用语法。
🤝 Promise.all 实现
8/11/26 【修正代码】原内容:只支持带 length/forEach 的数组,且空输入永远 pending;修改点:先接收任意 iterable,并让空输入立即 fulfilled 为 []。Promise.all 接收一个 Promise 可迭代对象(如数组),并返回一个新的 Promise。
- 成功:当所有传入的 Promise 都成功时,新 Promise 才会成功,结果是一个按原始顺序排列的值数组。
- 失败:只要有一个 Promise 失败,新 Promise 就会立即失败,并带上第一个失败的原因。
Promise.myAll = function(iterable) {
return new Promise((resolve, reject) => {
const promises = Array.from(iterable);
const results = new Array(promises.length);
let completedCount = 0;
if (promises.length === 0) {
resolve([]);
return;
}
promises.forEach((promise, index) => {
Promise.resolve(promise).then(value => {
results[index] = value;
completedCount++;
if (completedCount === promises.length) resolve(results);
}, reject);
});
});
};
🏎️ Promise.race 实现
Promise.race 同样接收一个 Promise 可迭代对象,并返回一个新的 Promise。
- 竞速:返回的 Promise 会随着第一个 Promise 的敲定(无论是成功还是失败)而敲定。
Promise.myRace = function(promises) {
return new Promise((resolve, reject) => {
// 1. 遍历所有 Promise
promises.forEach(promise => {
// 2. 使用 Promise.resolve 包装,确保处理非 Promise 值
Promise.resolve(promise)
.then(resolve) // 3. 任何一个 Promise 成功,立即 resolve
.catch(reject); // 4. 任何一个 Promise 失败,立即 reject
});
});
};
✨ Promise.any 实现
8/11/26 【修正代码】原内容:空输入永远 pending,错误按完成顺序 push;修改点:空输入拒绝为 AggregateError,错误按输入索引保存。Promise.any 是 ES2021 引入的新方法,它接收一个 Promise 可迭代对象,并返回一个新的 Promise。
- 成功:只要有一个 Promise 成功,新 Promise 就会立即成功,并带上第一个成功的值。
- 失败:只有当所有传入的 Promise 都失败时,新 Promise 才会失败,并带有一个
AggregateError,其中包含了所有失败的原因。
Promise.myAny = function(iterable) {
return new Promise((resolve, reject) => {
const promises = Array.from(iterable);
const errors = new Array(promises.length);
let rejectedCount = 0;
if (promises.length === 0) {
reject(new AggregateError([], 'All promises were rejected'));
return;
}
promises.forEach((promise, index) => {
Promise.resolve(promise).then(resolve, error => {
errors[index] = error;
rejectedCount++;
if (rejectedCount === promises.length) {
reject(new AggregateError(errors, 'All promises were rejected'));
}
});
});
});
};
📝 总结
| 方法 | 成功条件 | 失败条件 | 返回结果 |
|---|---|---|---|
| Promise.all | 所有 Promise 都成功 | 任一 Promise 失败 | 所有成功值的数组(按原始顺序) |
| Promise.race | 第一个 Promise 敲定(成功或失败) | 第一个 Promise 敲定(成功或失败) | 第一个敲定 Promise 的值或原因 |
| Promise.any | 第一个 Promise 成功 | 所有 Promise 都失败 | 第一个成功的值,或包含所有错误的 AggregateError |
正如我们之前提到的,JavaScript 原生并没有 Promise.map 方法。这个概念通常源于像 Bluebird 这样的第三方 Promise 库。 |
实现一个 Promise.map 的核心思路是:将一个数组,通过一个返回 Promise 的函数,转换成一个新的 Promise,这个新 Promise 的结果是一个由所有子 Promise 结果组成的数组。
我们可以利用之前讨论过的 Promise.all 来优雅地实现它。
🛠️ 实现 Promise.map
Promise.map 通常会接收两个参数:
- 一个数组(或可迭代对象)。
- 一个“映射函数”,该函数会对数组中的每个元素进行处理,并返回一个 Promise。
它的实现非常简单,核心就是将 Array.prototype.map 和 Promise.all 结合起来。
/**
* 实现一个 Promise.map 方法
* @param {Array} list - 需要处理的数组
* @param {Function} iteratee - 映射函数,接收 (item, index) 并应返回一个 Promise
* @returns {Promise} - 一个 Promise,resolve 时返回所有结果的数组
*/
Promise.myMap = function(list, iteratee) {
// 1. 使用原生 Array.map 遍历列表,对每个元素应用 iteratee 函数
// 这会生成一个由 Promise 组成的新数组
const promisesArray = list.map((item, index) => {
return iteratee(item, index);
});
// 2. 使用 Promise.all 等待这个 Promise 数组中的所有 Promise 完成
// Promise.all 返回的 Promise 会在所有子 Promise 成功后 resolve,结果为值数组
return Promise.all(promisesArray);
};
📝 使用示例
下面是一个具体的例子,模拟一个异步操作(例如从 API 获取数据),并使用我们实现的 Promise.myMap 来处理一个 ID 列表。
// 模拟一个异步函数,比如从服务器获取用户信息
function fetchUser(id) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (id > 0) {
resolve({ id: id, name: `用户${id}` });
} else {
reject(new Error(`无效的用户ID: ${id}`));
}
}, 1000);
});
}
const userIds = [1, 2, 3];
// 使用我们实现的 Promise.myMap
Promise.myMap(userIds, (id) => {
// 这里的函数会返回一个 Promise
console.log(`正在获取 ID 为 ${id} 的用户...`);
return fetchUser(id);
})
.then(users => {
console.log('所有用户获取成功:', users);
// 输出: 所有用户获取成功: [ { id: 1, name: '用户1' }, { id: 2, name: '用户2' }, { id: 3, name: '用户3' } ]
})
.catch(error => {
console.error('获取用户失败:', error);
// 如果任何一个 fetchUser 失败,这里会捕获到错误
});
✨ 进阶:增加并发限制
8/11/26 【修正代码】原内容:空数组不 resolve、非法并发数会卡住、同步抛错与拒绝后继续调度未处理;修改点:补齐输入转换、边界校验、空输入、同步错误和停止调度。 一个更强大的 Promise.map(如 Bluebird 库中的实现)通常会支持并发限制,即同时最多只执行 N 个 Promise。这对于处理大量任务,避免耗尽系统资源(如内存或网络连接)非常有用。
实现带并发限制的 Promise.map 会更复杂一些,需要一个队列来管理任务的执行。
/**
* 实现一个带并发限制的 Promise.map
* @param {Array} list - 需要处理的数组
* @param {Function} iteratee - 映射函数
* @param {Object} options - 选项,例如 { concurrency: 2 } 表示最多同时执行2个
* @returns {Promise}
*/
Promise.myMapWithConcurrency = function(iterable, iteratee, options = {}) {
const list = Array.from(iterable);
const concurrency = options.concurrency ?? Infinity;
if (!(concurrency === Infinity || (Number.isInteger(concurrency) && concurrency > 0))) {
return Promise.reject(new RangeError('concurrency must be a positive integer or Infinity'));
}
const results = new Array(list.length);
let nextIndexToProcess = 0;
let completedCount = 0;
let activeCount = 0;
let settled = false;
return new Promise((resolve, reject) => {
if (list.length === 0) {
resolve([]);
return;
}
function startNext() {
while (!settled && nextIndexToProcess < list.length && activeCount < concurrency) {
const index = nextIndexToProcess++;
activeCount++;
Promise.resolve()
.then(() => iteratee(list[index], index))
.then(value => {
results[index] = value;
completedCount++;
activeCount--;
if (completedCount === list.length) {
settled = true;
resolve(results);
} else {
startNext();
}
}, error => {
settled = true;
reject(error);
});
}
}
startNext();
});
};
📌 总结
- 基础实现:
Promise.map(list, fn)的核心就是Promise.all(list.map(fn))。它简单易用,但会同时启动所有异步任务。 - 进阶实现:带并发限制的
Promise.map更加健壮,适合在生产环境中处理大量任务,通过一个内部的任务队列和计数器来精确控制同时运行的 Promise 数量。
廖雪峰教程 · 7浏览器/7.6 async
async
可以用关键字async配合await调用Promise,实现异步操作,但代码却和同步写法类似:
async function get(url) {
let resp = await fetch(url);
let result = await resp.json();
return result;
}
使用async function可以定义一个异步函数,异步函数和Promise可以看作是等价的,在async function内部,用await调用另一个异步函数,写起来和同步代码没啥区别,但执行起来是异步的:
let resp = await fetch(url);
自动实现了异步调用,它和下面的Promise代码等价:
let promise = fetch(url);
promise.then((resp) => {
// 拿到resp
});
如果我们要实现catch()怎么办?用Promise的写法如下:
let promise = fetch(url);
promise.then((resp) => {
// 拿到resp
}).catch(e => {
// 出错了
});
用await调用时,直接用传统的try { ... } catch:
async function get(url) {
try {
let resp = await fetch(url);
let result = await resp.json();
return result;
} catch (e) {
// 出错了
}
}
用async定义异步函数,用await调用异步函数,写起来和同步代码差不多,但可读性大大提高。